/*
 * Copyright (C) 2009 aileron.cc
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package cc.aileron.orm;

import static cc.aileron.orm.factory.G2DaoTransactionManager.Category.*;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.SQLException;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import cc.aileron.commons.resource.ResourceNotFoundException;
import cc.aileron.commons.util.ClassPattrnFinderUtils;
import cc.aileron.orm.annotation.G2Transactional;
import cc.aileron.orm.bind.G2DaoBinder;
import cc.aileron.orm.bind.G2DaoManagerImpl;
import cc.aileron.orm.factory.G2DaoTransactionManager;
import cc.aileron.orm.factory.G2DaoTransactionManagerImpl;
import cc.aileron.orm.factory.serial.G2DaoSerial;
import cc.aileron.orm.factory.serial.G2DaoSerialGeneratedKeys;
import cc.aileron.orm.factory.serial.G2DaoSerialNoneGeneratedKeys;
import cc.aileron.orm.sql.template.SqlTemplateConfigure;
import cc.aileron.template.TemplateModule;
import cc.aileron.template.parser.ParserMethodNotFoundException;
import cc.aileron.template.reader.TemplateSyntaxEexception;

import com.google.inject.Binder;
import com.google.inject.Guice;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.matcher.Matchers;

/**
 * @author Aileron
 */
public class G2DaoModule implements
    Provider<Connection>,
    MethodInterceptor,
    Module
{
    @Override
    public void configure(final Binder binder)
    {
        /*
         * install module
         */
        binder.install(module);

        /*
         * bind connection to provider
         */
        binder.bind(Connection.class)
            .toProvider(this);

        /*
         * bind transaction manager to instance
         */
        binder.bind(G2DaoTransactionManager.class)
            .toInstance(transactionManager);

        /*
         * bind intercept
         */
        binder.bindInterceptor(Matchers.any(),
                Matchers.annotatedWith(annotation), this);
    }

    @Override
    public Connection get()
    {
        return transactionManager.get();
    }

    @Override
    public Object invoke(final MethodInvocation invocation) throws Throwable
    {
        try
        {
            transactionManager.start();
            final Object result = invocation.proceed();
            transactionManager.end(COMMIT);
            return result;
        }
        catch (final Throwable t)
        {
            transactionManager.end(ROLLBACK);
            throw t;
        }
        finally
        {
            transactionManager.close();
        }
    }

    /**
     * @param annotation
     * @param connection
     * @param targets
     * @throws Exception
     */
    public G2DaoModule(
        final Class<? extends Annotation> annotation,
        final Provider<Connection> connection,
        final Class<?>... targets) throws Exception
    {
        this.module = new G2DaoConnectionModule(connection, targets);
        this.annotation = annotation;
        this.transactionManager = new G2DaoTransactionManagerImpl(connection);
    }

    /**
     * @param connection
     * @param targets
     * @throws Exception
     */
    public G2DaoModule(
        final Provider<Connection> connection,
        final Class<?>... targets) throws Exception
    {
        this.module = new G2DaoConnectionModule(connection, targets);
        this.annotation = G2Transactional.class;
        this.transactionManager = new G2DaoTransactionManagerImpl(connection);
    }

    /**
     * annotation
     */
    private final Class<? extends Annotation> annotation;

    /**
     * module
     */
    private final G2DaoConnectionModule module;

    /**
     * c-provider
     */
    private final G2DaoTransactionManager transactionManager;
}

/**
 * @author Aileron
 */
class G2DaoConnectionModule implements Module
{
    @Override
    public void configure(final Binder binder)
    {
        binder.bind(G2DaoBinder.class)
            .toInstance(g2binder);

        binder.bind(G2DaoManager.class)
            .to(G2DaoManagerImpl.class)
            .asEagerSingleton();
    }

    /**
     * @return g2DaoBinder
     * @throws IOException
     * @throws URISyntaxException
     * @throws ResourceNotFoundException
     * @throws TemplateSyntaxEexception
     * @throws ParserMethodNotFoundException
     * @throws SQLException
     */
    private G2DaoBinder createBinder() throws IOException,
            URISyntaxException,
            ResourceNotFoundException,
            TemplateSyntaxEexception,
            ParserMethodNotFoundException,
            SQLException
    {
        final String name = connection.get()
            .getMetaData()
            .getDatabaseProductName();

        logger.trace("dbName : {}", name);

        final G2DbName dbName = G2DbName.convert(name);
        final Module dbModule = new Module()
        {
            @Override
            public void configure(final Binder binder)
            {
                binder.bind(G2DaoSerial.class)
                    .to(dbName.getSerial());
            }
        };

        final Module templateModule = new TemplateModule(
                SqlTemplateConfigure.configure);

        final G2DaoBinder g2binder = Guice.createInjector(dbModule,
                templateModule)
            .getInstance(G2DaoBinder.class);

        for (final Class<?> packageClass : targets)
        {
            for (final Class<?> targetClass : ClassPattrnFinderUtils.getClassNameList(packageClass.getPackage()))
            {
                g2binder.bind(targetClass);
            }
        }

        return g2binder;
    }

    /**
     * @param connection
     * @param targets
     * @throws Exception
     */
    public G2DaoConnectionModule(
        final Provider<Connection> connection,
        final Class<?>... targets) throws Exception
    {
        this.connection = connection;
        this.targets = targets;
        this.g2binder = createBinder();
    }

    private final Class<?>[] targets;

    private final Provider<Connection> connection;

    private final G2DaoBinder g2binder;

    private final Logger logger = LoggerFactory.getLogger(this.getClass());
}

/**
 * DB名の列挙型
 * 
 * @author Aileron
 */
enum G2DbName
{
    /**
     * mysql
     */
    MYSQL,

    /**
     * h2
     */
    H2,

    /**
     * postgresql
     */
    POSTGRESQL,

    /**
     * unknown
     */
    UNKNOWN;

    /**
     * @param value
     * @return DbName
     */
    public static G2DbName convert(final String value)
    {
        try
        {
            return valueOf(value.toUpperCase());
        }
        catch (final Exception e)
        {
            throw new NotSupportDataBase(value);
        }
    }

    /**
     * @return シリアル関連のクラス
     */
    public Class<? extends G2DaoSerial> getSerial()
    {
        switch (this)
        {
        case POSTGRESQL:
            return G2DaoSerialNoneGeneratedKeys.class;

        case H2:
        case MYSQL:
            return G2DaoSerialGeneratedKeys.class;

        default:
            return null;
        }
    }
}

/**
 * G2Daoサポート対象外のDB
 * 
 * @author Aileron
 */
class NotSupportDataBase extends Error
{
    private static final long serialVersionUID = -3903618007455579333L;

    public NotSupportDataBase(final String name)
    {
        super(name);
    }
}

/**
 * @author Aileron
 */
class TransactionCounter
{
    /**
     * @return this
     */
    public TransactionCounter countDown()
    {
        count -= 1;
        return this;
    }

    /**
     * @return this
     */
    public TransactionCounter countUp()
    {
        count += 1;
        return this;
    }

    /**
     * @return isZero
     */
    public boolean isZero()
    {
        return count == 0;
    }

    @Override
    public String toString()
    {
        return String.valueOf(count);
    }

    int count = 0;
}